/*
 * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cn.matezk.dms.system.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.matezk.dms.common.constant.SysConstants;
import cn.matezk.dms.common.context.UserContextHolder;
import cn.matezk.dms.common.enums.DisEnableStatusEnum;
import cn.matezk.dms.system.mapper.DeptMapper;
import cn.matezk.dms.system.mapper.UserMapper;
import cn.matezk.dms.system.mapper.UserSocialMapper;
import cn.matezk.dms.system.model.entity.DeptDO;
import cn.matezk.dms.system.model.query.DeptQuery;
import cn.matezk.dms.system.model.req.DeptReq;
import cn.matezk.dms.system.model.resp.DeptResp;
import cn.matezk.dms.system.service.DeptService;
import cn.matezk.dms.system.service.RoleDeptService;
import cn.matezk.dms.system.service.UserService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.validation.CheckUtils;
import top.continew.starter.data.core.enums.DatabaseType;
import top.continew.starter.data.core.util.MetaUtils;
import top.continew.starter.extension.crud.annotation.TreeField;
import top.continew.starter.extension.crud.autoconfigure.CrudProperties;
import top.continew.starter.extension.crud.autoconfigure.CrudTreeProperties;
import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.service.impl.BaseServiceImpl;

import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * 组织业务实现
 *
 * @author Charles7c
 * @since 2023/1/22 17:55
 */
@Service
@RequiredArgsConstructor
public class DeptServiceImpl extends BaseServiceImpl<DeptMapper, DeptDO, DeptResp, DeptResp, DeptQuery, DeptReq> implements DeptService {

    private final RoleDeptService roleDeptService;
    @Resource
    private UserService userService;
    @Resource
    private DataSource dataSource;
    @Resource
    private UserSocialMapper userSocialMapper;
    @Resource
    private UserMapper userMapper;

    @Override
    public void beforeAdd(DeptReq req) {
        String name = req.getName();
        boolean isExists = this.isNameExists(name, req.getParentId(), null);
        CheckUtils.throwIf(isExists, "新增失败，[{}] 已存在", name);
        req.setAncestors(this.getAncestors(req.getParentId()));
    }

    @Override
    public void beforeUpdate(DeptReq req, Long id) {
        String name = req.getName();
        boolean isExists = this.isNameExists(name, req.getParentId(), id);
        CheckUtils.throwIf(isExists, "修改失败，[{}] 已存在", name);
        DeptDO oldDept = super.getById(id);
        String oldName = oldDept.getName();
        DisEnableStatusEnum newStatus = req.getStatus();
        Long oldParentId = oldDept.getParentId();
        if (Boolean.TRUE.equals(oldDept.getIsSystem())) {
            CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, newStatus, "[{}] 是系统内置组织，不允许禁用", oldName);
            CheckUtils.throwIfNotEqual(req.getParentId(), oldParentId, "[{}] 是系统内置组织，不允许变更上级组织", oldName);
        }
        // 启用/禁用组织
        if (ObjectUtil.notEqual(newStatus, oldDept.getStatus())) {
            List<DeptDO> children = this.listChildren(id);
            long enabledChildrenCount = children.stream()
                .filter(d -> DisEnableStatusEnum.ENABLE.equals(d.getStatus()))
                .count();
            CheckUtils.throwIf(DisEnableStatusEnum.DISABLE
                .equals(newStatus) && enabledChildrenCount > 0, "禁用 [{}] 前，请先禁用其所有下级组织", oldName);
            DeptDO oldParentDept = this.getByParentId(oldParentId);
            CheckUtils.throwIf(DisEnableStatusEnum.ENABLE.equals(newStatus) && DisEnableStatusEnum.DISABLE
                .equals(oldParentDept.getStatus()), "启用 [{}] 前，请先启用其所有上级组织", oldName);
        }
        // 变更上级组织
        if (ObjectUtil.notEqual(req.getParentId(), oldParentId)) {
            // 更新祖级列表
            String newAncestors = this.getAncestors(req.getParentId());
            req.setAncestors(newAncestors);
            // 更新子级的祖级列表
            this.updateChildrenAncestors(newAncestors, oldDept.getAncestors(), id);
        }
    }

    @Override
    public void beforeDelete(List<Long> ids) {
        List<DeptDO> list = baseMapper.lambdaQuery()
            .select(DeptDO::getName, DeptDO::getIsSystem)
            .in(DeptDO::getId, ids)
            .list();
        Optional<DeptDO> isSystemData = list.stream().filter(DeptDO::getIsSystem).findFirst();
        CheckUtils.throwIf(isSystemData::isPresent, "所选组织 [{}] 是系统内置组织，不允许删除", isSystemData.orElseGet(DeptDO::new)
            .getName());
        CheckUtils.throwIf(this.countChildren(ids) > 0, "所选组织存在下级组织，不允许删除");
        CheckUtils.throwIf(userService.countByDeptIds(ids) > 0, "所选组织存在用户关联，请解除关联后重试");
        CheckUtils.throwIf(baseMapper.countDevice(ids) > 0, "所选组织存在设备关联，请解除关联后重试");
        // 删除角色和组织关联
        roleDeptService.deleteByDeptIds(ids);
    }

    @Override
    public List<DeptDO> listChildren(Long id) {
        DatabaseType databaseType = MetaUtils.getDatabaseTypeOrDefault(dataSource, DatabaseType.MYSQL);
        return baseMapper.lambdaQuery().apply(databaseType.findInSet(id, "ancestors")).list();
    }

    @Override
    public List<DeptDO> listByNames(List<String> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        return this.list(Wrappers.<DeptDO>lambdaQuery().in(DeptDO::getName, list));
    }

    @Override
    public int countByNames(List<String> deptNames) {
        if (CollUtil.isEmpty(deptNames)) {
            return 0;
        }
        return (int)this.count(Wrappers.<DeptDO>lambdaQuery().in(DeptDO::getName, deptNames));
    }

    /**
     * 名称是否存在
     *
     * @param name     名称
     * @param parentId 上级 ID
     * @param id       ID
     * @return 是否存在
     */
    private boolean isNameExists(String name, Long parentId, Long id) {
        return baseMapper.lambdaQuery()
            .eq(DeptDO::getName, name)
            .eq(DeptDO::getParentId, parentId)
            .ne(null != id, DeptDO::getId, id)
            .exists();
    }

    /**
     * 获取祖级列表
     *
     * @param parentId 上级组织
     * @return 祖级列表
     */
    private String getAncestors(Long parentId) {
        DeptDO parentDept = this.getByParentId(parentId);
        return "%s,%s".formatted(parentDept.getAncestors(), parentId);
    }

    /**
     * 根据上级组织 ID 查询
     *
     * @param parentId 上级组织 ID
     * @return 上级组织信息
     */
    private DeptDO getByParentId(Long parentId) {
        DeptDO parentDept = baseMapper.selectById(parentId);
        CheckUtils.throwIfNull(parentDept, "上级组织不存在");
        return parentDept;
    }

    /**
     * 查询子组织数量
     *
     * @param ids ID 列表
     * @return 子组织数量
     */
    private Long countChildren(List<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return 0L;
        }
        DatabaseType databaseType = MetaUtils.getDatabaseTypeOrDefault(dataSource, DatabaseType.MYSQL);
        return ids.stream()
            .mapToLong(id -> baseMapper.lambdaQuery().apply(databaseType.findInSet(id, "ancestors")).count())
            .sum();
    }

    /**
     * 更新子组织祖级列表
     *
     * @param newAncestors 新祖级列表
     * @param oldAncestors 原祖级列表
     * @param id           ID
     */
    private void updateChildrenAncestors(String newAncestors, String oldAncestors, Long id) {
        List<DeptDO> children = this.listChildren(id);
        if (CollUtil.isEmpty(children)) {
            return;
        }
        List<DeptDO> list = new ArrayList<>(children.size());
        for (DeptDO child : children) {
            DeptDO dept = new DeptDO();
            dept.setId(child.getId());
            dept.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors));
            list.add(dept);
        }
        baseMapper.updateById(list);
    }

    @Override
    protected QueryWrapper<DeptDO> buildQueryWrapper(DeptQuery query) {
        String key = query.getKey();
        DisEnableStatusEnum status = query.getStatus();
        return new QueryWrapper<DeptDO>().and(StrUtil.isNotBlank(key), q -> q.like("name", key)
            .or()
            .like("description", key))
            .eq(ObjectUtil.isNotNull(status), "status", ObjectUtil.isNull(status) ? null : status.getValue())
            .and(ObjectUtil.isNotNull(query.getUserDeptId()) && !SysConstants.SUPER_DEPT_ID.equals(query
                .getUserDeptId()), q -> {
                    List<Long> deptIdList = listChildren(query.getUserDeptId()).stream()
                        .map(DeptDO::getId)
                        .collect(Collectors.toList());
                    deptIdList.add(query.getUserDeptId());
                    q.in("id", deptIdList);
                });
    }

    @Override
    public List<Tree<Long>> tree(DeptQuery query, SortQuery sortQuery, boolean isSimple) {
        List<Tree<Long>> tree = buildTree(query, sortQuery, isSimple);
        if (SysConstants.DEPT_SCOPE_ALL.equals(query.getScope())) {
            List<Tree<Long>> appTree = buildMiniAppTree(sortQuery, isSimple);
            if (ObjectUtil.isNotNull(appTree)) {
                tree.addAll(appTree);
            }
        }
        return tree;
    }

    @Override
    public List<DeptResp> list(DeptQuery query, SortQuery sortQuery) {
        return this.list(query, sortQuery, this.getListClass());
    }

    private List<Tree<Long>> buildTree(DeptQuery query, SortQuery sortQuery, boolean isSimple) {
        query.setUserDeptId(UserContextHolder.getContext().getDeptId());
        if (SysConstants.WECHAT_DEPT_ID.equals(query.getUserDeptId()))
            return new ArrayList<>();
        DeptDO dept = baseMapper.selectById(UserContextHolder.getContext().getDeptId());
        DeptResp rootDept = new DeptResp();
        BeanUtil.copyProperties(dept, rootDept);
        CrudTreeProperties treeProperties = SpringUtil.getBean(CrudProperties.class).getTree();
        TreeField treeField = DeptResp.class.getDeclaredAnnotation(TreeField.class);
        TreeNodeConfig treeNodeConfig;
        treeNodeConfig = isSimple ? treeProperties.genTreeNodeConfig() : treeProperties.genTreeNodeConfig(treeField);
        Tree<Long> root = new Tree<>();
        root.setId(rootDept.getId());
        root.setParentId((rootDept.getParentId()));
        root.setName(rootDept.getName());
        root.setWeight((rootDept.getSort()));
        List<Field> fieldList = ReflectUtils.getNonStaticFields(DeptResp.class);
        fieldList.forEach((f) -> root.putExtra(f.getName(), ReflectUtil.invoke(rootDept, CharSequenceUtil.genGetter(f
            .getName()), new Object[0])));
        List<DeptResp> list = list(query, sortQuery);
        List<Tree<Long>> children = TreeUtil.build(list, rootDept.getId(), treeNodeConfig, (node, tree) -> {
            tree.setId(node.getId());
            tree.setParentId((node.getParentId()));
            tree.setName(node.getName());
            tree.setWeight((node.getName()));
            if (!isSimple) {
                fieldList.removeIf((f) -> CharSequenceUtil.equalsAnyIgnoreCase(f.getName(), new CharSequence[] {
                    treeField.value(), treeField.parentIdKey(), treeField.nameKey(), treeField.weightKey(), treeField
                        .childrenKey()}));
                fieldList.forEach((f) -> tree.putExtra(f.getName(), ReflectUtil.invoke(node, CharSequenceUtil
                    .genGetter(f.getName()), new Object[0])));
            }
        });
        root.setChildren(children);
        List<Tree<Long>> tree = new ArrayList<>();
        tree.add(root);
        return tree;
    }

    private List<Tree<Long>> buildMiniAppTree(SortQuery sortQuery, boolean isSimple) {
        Long userId = userMapper.miniAppUserId(UserContextHolder.getUserId());
        if (ObjectUtil.isNull(userId))
            return null;
        CrudTreeProperties treeProperties = SpringUtil.getBean(CrudProperties.class).getTree();
        TreeField treeField = DeptResp.class.getDeclaredAnnotation(TreeField.class);
        TreeNodeConfig treeNodeConfig;
        treeNodeConfig = isSimple ? treeProperties.genTreeNodeConfig() : treeProperties.genTreeNodeConfig(treeField);
        List<Field> fieldList = ReflectUtils.getNonStaticFields(DeptResp.class);
        List<DeptResp> list = list(new DeptQuery(), sortQuery).stream()
            .filter(item -> ObjectUtil.equal(item.getCreateUser(), userId))
            .collect(Collectors.toList());
        List<Tree<Long>> children = TreeUtil.build(list, SysConstants.SUPER_PARENT_ID, treeNodeConfig, (node, tree) -> {
            tree.setId(node.getId());
            tree.setParentId((node.getParentId()));
            tree.setName(node.getName());
            tree.setWeight((node.getName()));
            if (!isSimple) {
                fieldList.removeIf((f) -> CharSequenceUtil.equalsAnyIgnoreCase(f.getName(), new CharSequence[] {
                    treeField.value(), treeField.parentIdKey(), treeField.nameKey(), treeField.weightKey(), treeField
                        .childrenKey()}));
                fieldList.forEach((f) -> tree.putExtra(f.getName(), ReflectUtil.invoke(node, CharSequenceUtil
                    .genGetter(f.getName()), new Object[0])));
            }
        });
        return children;
    }
}
